Popular Searches
Popular Course Categories
Popular Courses

Flutter TextEditingController

Flutter TextEditingController

Flutter Forms & User Input

Flutter TextEditingController – Detailed Notes

TextEditingController is a Flutter class used to control, read, modify, clear, and listen to the text entered inside a TextField or TextFormField. It is especially useful when an application needs to access user input programmatically.

Flutter's official documentation describes TextEditingController as a controller for an editable text field. It can provide an initial value, expose the current text and selection, notify listeners when the value changes, and update the text field programmatically.


1. What is TextEditingController?

A TextEditingController acts as a bridge between your Dart code and a text input widget.

It allows you to:

  • Read text entered by the user.
  • Set an initial value.
  • Change the text programmatically.
  • Clear the text field.
  • Listen for text changes.
  • Access the current selection.
  • Use text input in login, registration, search, profile, and other forms.

Basic Syntax

final TextEditingController controller = TextEditingController();

2. Import Required Package

For Material-based Flutter applications, import:

import 'package:flutter/material.dart';

3. Creating a TextEditingController

Create a controller as a field inside the State class when it belongs to a stateful form or screen.

class MyScreenState extends State {
  final TextEditingController nameController = TextEditingController();

  @override
  void dispose() {
    nameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

The controller should generally be disposed when it is no longer needed. This releases resources associated with the controller.


4. Connecting TextEditingController to TextField

Use the controller property of TextField.

final TextEditingController nameController = TextEditingController();

TextField(
  controller: nameController,
)

After connecting the controller, the controller can be used to retrieve or modify the text.


5. Reading Text from TextEditingController

The text property returns the current text.

String name = nameController.text;
print(name);

Complete Example

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const UserInputScreen(),
    );
  }
}

class UserInputScreen extends StatefulWidget {
  const UserInputScreen({super.key});

  @override
  State createState() => _UserInputScreenState();
}

class _UserInputScreenState extends State {
  final TextEditingController nameController = TextEditingController();

  @override
  void dispose() {
    nameController.dispose();
    super.dispose();
  }

  void showName() {
    print(nameController.text);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TextEditingController'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: nameController,
              decoration: const InputDecoration(
                labelText: 'Enter your name',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: showName,
              child: const Text('Read Name'),
            ),
          ],
        ),
      ),
    );
  }
}

6. Getting Text When Button is Pressed

A common use case is collecting information after the user presses a button.

final TextEditingController emailController = TextEditingController();

ElevatedButton(
  onPressed: () {
    String email = emailController.text;
    print('Email: $email');
  },
  child: const Text('Submit'),
)

7. Setting Initial Text

You can create a controller with initial text by using the text parameter.

final TextEditingController nameController =
    TextEditingController(text: 'Manish');

When this controller is connected to a TextField, the field initially displays Manish.

Example

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State {
  final TextEditingController nameController =
      TextEditingController(text: 'John');

  @override
  void dispose() {
    nameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: TextField(
          controller: nameController,
          decoration: const InputDecoration(
            labelText: 'Name',
            border: OutlineInputBorder(),
          ),
        ),
      ),
    );
  }
}

8. Changing Text Programmatically

You can change the current text using the text property.

nameController.text = 'Flutter Developer';

For example:

ElevatedButton(
  onPressed: () {
    nameController.text = 'Flutter Developer';
  },
  child: const Text('Set Text'),
)

Changing the controller's text updates the connected text field.


9. Clearing Text

The clear() method removes the current text.

nameController.clear();

Example

ElevatedButton(
  onPressed: () {
    nameController.clear();
  },
  child: const Text('Clear'),
)

This is commonly used after submitting a message, search query, or todo item.


10. Checking Whether Text is Empty

You can check whether the user has entered anything.

if (nameController.text.isEmpty) {
  print('Name is required');
} else {
  print('Name: ${nameController.text}');
}

Using trim()

trim() removes unnecessary whitespace from the beginning and end of a string.

final String name = nameController.text.trim();

if (name.isEmpty) {
  print('Please enter your name');
}

11. Listening to Text Changes

A TextEditingController can notify your code whenever its value changes. Use addListener() to register a listener.

final TextEditingController searchController =
    TextEditingController();

@override
void initState() {
  super.initState();

  searchController.addListener(() {
    print(searchController.text);
  });
}

The listener runs whenever the controller's value changes.

Complete Listener Example

class SearchScreen extends StatefulWidget {
  const SearchScreen({super.key});

  @override
  State createState() => _SearchScreenState();
}

class _SearchScreenState extends State {
  final TextEditingController searchController =
      TextEditingController();

  @override
  void initState() {
    super.initState();

    searchController.addListener(_handleSearch);
  }

  void _handleSearch() {
    print('Search: ${searchController.text}');
  }

  @override
  void dispose() {
    searchController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: TextField(
          controller: searchController,
          decoration: const InputDecoration(
            hintText: 'Search',
            border: OutlineInputBorder(),
          ),
        ),
      ),
    );
  }
}

12. TextEditingController vs onChanged

Both onChanged and TextEditingController can be used to detect changes in text.

Feature onChanged TextEditingController
Read current text Yes Yes
Set text programmatically No Yes
Clear field programmatically No Yes
Listen to changes Yes Yes
Access selection No Yes
Initial value Not directly Yes
Requires dispose() No Yes when owned by your State/widget lifecycle

onChanged Example

TextField(
  onChanged: (value) {
    print(value);
  },
)

Controller Example

final controller = TextEditingController();

TextField(
  controller: controller,
)

print(controller.text);

13. Using TextEditingController with TextFormField

TextEditingController can also be connected to TextFormField.

final TextEditingController emailController =
    TextEditingController();

TextFormField(
  controller: emailController,
  decoration: const InputDecoration(
    labelText: 'Email',
    border: OutlineInputBorder(),
  ),
)

This is especially useful when working with forms and validation.


14. TextEditingController with Form Validation

A controller can be used to retrieve the validated input after the form has been successfully validated.

final _formKey = GlobalKey();
final emailController = TextEditingController();

Form(
  key: _formKey,
  child: TextFormField(
    controller: emailController,
    validator: (value) {
      if (value == null || value.trim().isEmpty) {
        return 'Email is required';
      }
      return null;
    },
  ),
)

Submitting the Form

if (_formKey.currentState!.validate()) {
  print(emailController.text);
}

15. Multiple TextEditingControllers

In a registration or profile form, you can create separate controllers for each input field.

final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();

Using the Controllers

Column(
  children: [
    TextField(
      controller: nameController,
      decoration: const InputDecoration(
        labelText: 'Name',
      ),
    ),
    TextField(
      controller: emailController,
      decoration: const InputDecoration(
        labelText: 'Email',
      ),
    ),
    TextField(
      controller: phoneController,
      decoration: const InputDecoration(
        labelText: 'Phone',
      ),
    ),
    TextField(
      controller: passwordController,
      obscureText: true,
      decoration: const InputDecoration(
        labelText: 'Password',
      ),
    ),
  ],
)

16. Disposing Multiple Controllers

Every controller owned by the screen should be disposed when the screen is removed.

@override
void dispose() {
  nameController.dispose();
  emailController.dispose();
  phoneController.dispose();
  passwordController.dispose();
  super.dispose();
}

This is an important part of managing controller resources correctly.


17. Complete Registration Form Example

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const RegistrationScreen(),
    );
  }
}

class RegistrationScreen extends StatefulWidget {
  const RegistrationScreen({super.key});

  @override
  State createState() => _RegistrationScreenState();
}

class _RegistrationScreenState extends State {
  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final phoneController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    phoneController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  void registerUser() {
    final name = nameController.text.trim();
    final email = emailController.text.trim();
    final phone = phoneController.text.trim();
    final password = passwordController.text;

    print('Name: $name');
    print('Email: $email');
    print('Phone: $phone');
    print('Password: $password');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Registration'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: nameController,
              decoration: const InputDecoration(
                labelText: 'Full Name',
                prefixIcon: Icon(Icons.person),
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
                prefixIcon: Icon(Icons.email),
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: phoneController,
              keyboardType: TextInputType.phone,
              decoration: const InputDecoration(
                labelText: 'Phone',
                prefixIcon: Icon(Icons.phone),
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
                prefixIcon: Icon(Icons.lock),
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 20),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: registerUser,
                child: const Text('Register'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

18. Login Form Example

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State createState() => _LoginScreenState();
}

class _LoginScreenState extends State {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  void login() {
    final email = emailController.text.trim();
    final password = passwordController.text;

    if (email.isEmpty || password.isEmpty) {
      print('Please enter email and password');
      return;
    }

    print('Login Email: $email');
    print('Login Password: $password');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: login,
              child: const Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

19. Building a Search Field

One practical use of TextEditingController is creating a search interface.

final searchController = TextEditingController();

TextField(
  controller: searchController,
  decoration: InputDecoration(
    hintText: 'Search products',
    prefixIcon: const Icon(Icons.search),
    suffixIcon: IconButton(
      icon: const Icon(Icons.clear),
      onPressed: () {
        searchController.clear();
      },
    ),
    border: const OutlineInputBorder(),
  ),
)

Reading Search Text

void searchProducts() {
  final query = searchController.text.trim();

  if (query.isEmpty) {
    return;
  }

  print('Searching for: $query');
}

20. Updating Text from a Button

A controller is useful when the application needs to change input automatically.

ElevatedButton(
  onPressed: () {
    nameController.text = 'Flutter Student';
  },
  child: const Text('Use Default Name'),
)

21. Resetting a Form

You can clear multiple controllers to reset a form.

void resetForm() {
  nameController.clear();
  emailController.clear();
  phoneController.clear();
  passwordController.clear();
}

Reset Button

OutlinedButton(
  onPressed: resetForm,
  child: const Text('Reset'),
)

22. TextEditingController and Text Selection

The controller also provides access to the current text selection through the selection property.

print(nameController.selection);

You can use selection information when building advanced text-editing features.

Select All Text

nameController.selection = TextSelection(
  baseOffset: 0,
  extentOffset: nameController.text.length,
);

23. Using TextEditingValue

TextEditingController extends Flutter's value-notifier system and exposes the complete editing state through the value property.

print(nameController.value);

The value can contain information such as:

  • Current text.
  • Selection.
  • Composing range.

Example

nameController.value = TextEditingValue(
  text: 'Flutter',
  selection: const TextSelection.collapsed(offset: 7),
);

Using value can be useful when you need to update text together with selection information.


24. Important Warning About Listeners

If you modify the controller inside its own listener, the listener may be triggered again. This can create an unintended loop.

Problematic Pattern

controller.addListener(() {
  controller.text = controller.text.toUpperCase();
});

This pattern can repeatedly modify the controller while the listener is responding to its own changes.

Better Approach

For simple transformations, consider using input formatters or carefully guard the listener so that it only changes the value when necessary.


25. TextEditingController with Input Formatting

For restrictions such as numbers, length limits, or specific character patterns, Flutter provides input formatters.

TextField(
  controller: phoneController,
  keyboardType: TextInputType.phone,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
  ],
)

When using this example, import:

import 'package:flutter/services.dart';

26. Practical Example: Character Counter

A controller can be used with a listener to update a character count.

class MessageScreen extends StatefulWidget {
  const MessageScreen({super.key});

  @override
  State createState() => _MessageScreenState();
}

class _MessageScreenState extends State {
  final messageController = TextEditingController();

  @override
  void initState() {
    super.initState();
    messageController.addListener(_updateCount);
  }

  void _updateCount() {
    setState(() {});
  }

  @override
  void dispose() {
    messageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: messageController,
              maxLines: 5,
              decoration: const InputDecoration(
                labelText: 'Message',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 10),
            Text(
              'Characters: ${messageController.text.length}',
            ),
          ],
        ),
      ),
    );
  }
}

27. Practical Example: Todo Input

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State createState() => _TodoScreenState();
}

class _TodoScreenState extends State {
  final controller = TextEditingController();
  final List todos = [];

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  void addTodo() {
    final text = controller.text.trim();

    if (text.isEmpty) {
      return;
    }

    setState(() {
      todos.add(text);
    });

    controller.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo App'),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: controller,
                    decoration: const InputDecoration(
                      hintText: 'Enter a task',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: addTodo,
                  child: const Text('Add'),
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                return ListTile(
                  leading: const Icon(Icons.check_circle_outline),
                  title: Text(todos[index]),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

28. TextEditingController Lifecycle

A typical controller lifecycle contains three important stages:

  1. Create: Create the controller as a state field.
  2. Use: Connect it to a TextField or TextFormField.
  3. Dispose: Call dispose() when the owning widget is removed.
class ExampleState extends State {
  final controller = TextEditingController();

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
    );
  }
}

29. Common Mistakes

Mistake 1: Forgetting dispose()

final controller = TextEditingController();

// Missing dispose()

Better:

@override
void dispose() {
  controller.dispose();
  super.dispose();
}

Mistake 2: Creating the Controller Inside build()

Avoid creating a new controller every time build() runs.

@override
Widget build(BuildContext context) {
  final controller = TextEditingController();
  return TextField(controller: controller);
}

This can cause the controller to be recreated during rebuilds.

Better: Keep the controller as a field of the appropriate State object.

Mistake 3: Reading the Wrong Controller

When multiple fields are present, use descriptive controller names.

final emailController = TextEditingController();
final passwordController = TextEditingController();

Mistake 4: Not Trimming User Input

final name = nameController.text.trim();

Using trim() can prevent accidental leading or trailing spaces from being treated as meaningful input.


30. Best Practices

  • Use a separate controller for each field when you need programmatic access to that field.
  • Dispose controllers that your widget owns.
  • Use trim() when appropriate before processing user-entered text.
  • Use TextFormField with a Form when structured validation is required.
  • Use onChanged for simple change handling when a controller is not otherwise needed.
  • Use addListener() when you need centralized observation of controller changes.
  • Avoid unnecessary controller creation inside build().
  • Use input formatters for input restrictions rather than manually modifying text whenever possible.
  • Be careful when changing controller values from inside listeners.
  • Use meaningful controller names such as emailController and passwordController.

31. TextEditingController Important Properties and Methods

Property/Method Purpose
text Gets or sets the current text.
value Gets or sets the complete editing value.
selection Gets or sets the current text selection.
clear() Removes the current text.
addListener() Registers a callback for controller changes.
removeListener() Removes a previously registered listener.
dispose() Releases resources when the controller is no longer needed.
hasListeners Indicates whether listeners are registered.

32. TextField and TextEditingController Relationship

TextEditingController
        |
        | controller:
        v
TextField / TextFormField
        |
        v
User enters text
        |
        v
Controller value changes
        |
        v
Dart code reads or modifies the value

The controller provides programmatic control over the editable text field.


33. Real-World Applications

TextEditingController is commonly useful in:

  • Login forms.
  • Registration forms.
  • Search bars.
  • Profile editing screens.
  • Chat applications.
  • Comment sections.
  • Todo applications.
  • Contact forms.
  • Checkout forms.
  • Address forms.
  • Product search.
  • Data submission screens.
  • API request forms.

34. Interview Questions

Q1. What is TextEditingController?

Answer: It is a Flutter controller used to control and access the text and editing state of a TextField or TextFormField.

Q2. How do you get text from a controller?

String text = controller.text;

Q3. How do you clear a TextField?

controller.clear();

Q4. How do you set text programmatically?

controller.text = 'Hello Flutter';

Q5. Why should a controller be disposed?

Answer: Controllers are disposable objects. Calling dispose() when the owning widget is finished with the controller releases resources and removes registered listeners associated with it.

Q6. Can TextEditingController be used with TextFormField?

Answer: Yes. A controller can be supplied to both TextField and TextFormField.

Q7. What is the difference between onChanged and TextEditingController?

Answer: onChanged is a callback for receiving changed text, while TextEditingController provides broader programmatic control, including reading, changing, clearing, listening to, and inspecting the editing value.


35. Quick Revision

  • TextEditingController controls editable text.
  • Connect it using the controller property.
  • Read input with controller.text.
  • Set input with controller.text = '...'.
  • Clear input with controller.clear().
  • Listen for changes with controller.addListener().
  • Use controller.value for complete editing information.
  • Use controller.selection for selection management.
  • Dispose the controller when the owning widget is disposed.
  • Use controllers extensively in forms, search interfaces, login screens, registration screens, and data-entry applications.

36. Official Flutter Resources


37. JustAcademy Flutter Training Resources

Learn more about Flutter development through the following resources:


38. Key Takeaways

TextEditingController is one of the most important tools for working with user-entered text in Flutter. It provides direct access to the text field's current value and allows developers to read, update, clear, monitor, and manage text programmatically. It works with both TextField and TextFormField and is particularly useful for forms, authentication interfaces, search functionality, API data submission, and other interactive Flutter applications.

Previous Next
whatsapp